Emitter

Remember the advice from the beginning of the assignment! I really recommend not trying to implement everything all at once :).

1 Functions in x86-64

Here are a few reminders and notes about how functions work in x86-64.

  • The start of the function is labelled with the function name.
  • Before the call, the caller should save registers to the stack, if needed. After the call, the caller should restore any saved registers from the stack. These registers could include r10 and r11, as well as any registers that will store argument values for the function call and any other registers that may be overwritten by the callee (including rax).
  • The first six arguments are passed in registers rdi, rsi, rdx, rcx, r8, and r9 (in that order).
  • If a function defines more than six parameters, the additional parameters are pushed on the stack (in reverse order: last parameter first).
    • The caller is responsible for pushing these arguments onto the stack before invoking the function. This means that any stack-allocated argument values will be located at positive offsets from the base pointer (rbp), once control has transferred to the callee.
    • The caller is also responsible for removing these arguments from the stack after the function call returns, which typically involves adjusting the stack pointer accordingly.
  • The call instruction transfers control to the function and pushes the return address onto the stack.
  • The callee should save the registers that are designated as “callee-saved”, if needed (e.g., rbx, rbp, r12, r13, r14, r15). The values should be restored before returning.
  • The callee stores the return value of the function in the rax register.
  • The ret instruction returns control to the caller, by popping the return address from the stack and jumping to it.

2 Implementation advice

  • Remember to first make a plan, and document it in README.md!
  • Go slowly :).
  • Remember that it is always safe to save a register! It may be easier for the caller and callee to just save all the appropriate registers, regardless of whether they will be used.
  • The body of the function, including allocation/deallocation for local variables, can be handled essentially the same way as in the previous assignment. One major difference is that names can now refer to either local variables or parameters.
  • You could consider first implementing a simpler semantics for variables than the one described earlier in the assignment. In particular, it might be easier to treat all assignments in a function body as either creating a local variable or updating it. With these semantics, parameters are read-only.
  • You might be able to make use of the compiler monad’s state and avoid a lot of modifications to your existing code. For example, when allocating space or emitting code for the body of a function, you could make use of a currentFunction value in the compiler’s state. Alternatively, you could add a parameter to some of your allocator / emitter Haskell functions. In either case, you probably want to use a Maybe, so that you can handle the main body of the program, as well as the bodies of functions.